#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/ethernet.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


#define mes_len 256 //固定长度
char file_name[mes_len] = "";

int str_ex(char in_str[])
{
    char ex_str[mes_len] = "";//创建临时字符数组
    strcpy(ex_str,in_str);//cp传入的字符数组
    char *str1 = strtok(ex_str,".");//通过strtok获取每个部分的内容
    char *str2 = strtok(NULL,".");
    char *str3 = strtok(NULL,".");
    char *str4 = strtok(NULL,",");
    char *str5 = strtok(NULL,",");

    char ex_num[mes_len] = "";//创建一个字符数组,用来存储前面被分格出来的全部字符
    //因为strtok分格字符串时,如果全部的内容已经被前面的strtok分格完,那么当前的char*变量就等同于未初始化的指针变量,地址为NULL,指向的内容自然也为NULL
    if (str1!=NULL && str2!=NULL && str3!=NULL && str4!=NULL)//这里判断strtok分格出来的的字符串是否为NULL
    {
        sprintf(ex_num,"%s%s%s%s",str1,str2,str3,str4);//拼接全部分格出来的字符串,以供后面检查
    }
    else//如果有字符串为NULL,那么打印提示,并直接返回
    {
        printf("请按正确格式输入IP!\n");
        return -1;
    }
    for (int i = 0; i < strlen(ex_num); i++)
    {   //通过循环和下标,对前面拼接的字符串进行逐个检查,如果不为数字,就打印提示并返回
        if (ex_num[i]<'0'||ex_num[i]>'9')
        {
            printf("请按正确格式输入IP!\n");
            return -1;
        }
    }
    //判断是否输入了文件名
    if (str5==NULL)
    {
        printf("请输入文件名!\n");
        return -1;
    }
    
    //判断ip分格出来的字符串是否超过3位,因为ip的每个部分都是一个字节,最高只能到255
    if (strlen(str1)>3||strlen(str2)>3||strlen(str3)>3||strlen(str4)>3)
    {
        printf("请按正确范围输入IP!\n");
        return -1;
    }
    
    return 0;
}

int argv_ex(int argc_in,char const *argv_in)
{
    //此函数用来判断输入参数的个数及类型,所以需要一个参数,如果缺少参数或者多输入参数,则会打印提示并退出
    if ((argc_in != 2))
    {
        printf("输入格式不对!格式: a.out 端口\n");
        return -1;
    }
    for (int i = 0; i < strlen(argv_in); i++)
    {   //通过循环和下标,对输入的参数进行逐个检查,如果不为数字,就打印提示并返回
        if (argv_in[1]<'0'||argv_in[1]>'9')
        {
            printf("参数必须是数值!\n");
            return -1;
        }
    }
    //判断输入的参数是否在范围内,否则打印提示并退出
    if ((atoi(argv_in)>65535) || (atoi(argv_in)<1))
    {
        printf("输入参数不对!参数为端口,数值范围为1-65535!\n");
        return -1;
    }
    return 0;
}

void* recv_tftp(void* arg)
{
    int *sock_fd = (int*)arg;//通过强转获取外部传入的描述符
    int fd = open(file_name, O_CREAT | O_WRONLY | O_RDONLY, 777);//创建一个file_name的文件,并给予读写权限,返回一个文件描述符
    if (fd < 0)//判断文件描述符是否创建成功
    {
        perror("open");
        return 0;
    }
    int block = 0;//记录获取的资源块数
    while (1)
    {
        short block_num = 0;//获取数据包里面的块编号
        struct sockaddr_in s_addr;//接受服务器发来的ip和端口
        bzero(&s_addr,sizeof(s_addr));//初始化结构体
        socklen_t s_len = sizeof(s_addr);//获取结构体长度

        unsigned char buf[516] = "";//每次获取的数据
        //读取服务器数据
        int len = recvfrom(*sock_fd, buf, sizeof(buf), 0,(struct sockaddr *)&s_addr, &s_len);//获取资源,数据放到buf里面,ip和端口放到s_addr里面
        if (buf[1]==3)//通过判断接受数据的操作码,得知当前数据是否是数据包
        {
            block++;//记录资源块数
            char block_in[2]="";//获取块编号
            block_in[0]=buf[3];//因为字节序,所以第一个字节放块编号的第二个字节
            block_in[1]=buf[2];//同上原因,第二个字节放块编号的第一个字节
            block_num = *(unsigned short *)block_in;//强转块编号成无符号短整型
            printf("当前数据包是第%d个\n",block_num);//打印当前块编号
            write(fd,buf+4,len-4);//通过文件描述符往之前创建的文件里面写数据
            buf[1]=4;//通过修改操作码,给tftp服务器回应
            sendto(*sock_fd,buf,4,0,(struct sockaddr *)&s_addr,s_len);//向服务器发送ACK数据
        }
        else break;//回应的数据里面的操作码不对直接退出

        if (len<516)//判断当前数据包是否低于516字节,低于说明当前数据包为整个文件的最后一点数据
        {
            int block_all = (block_num-1)*512+len-4;//通过之前记录的资源块数,算出获取了多少字节的资源
            printf("本次传输一共获取%dBytes\n",block_all);//打印获取了多少字节的资源
            break;
        }
    }
    close(fd);//关闭文件描述符
    return 0;
}

void* send_tftp(void* arg)
{
    int *fd = (int*)arg;//通过强转获取外部传入的描述符
    while (1)//循环保证线程一直处于运行状态
    {
        char scanf_mes[mes_len] = "";//记录输入的内容
        scanf("%s",scanf_mes);//输入内容

        if (strcmp(scanf_mes,"close")==0)break;//判断输入的是否是关闭指令
        int ex = str_ex(scanf_mes);//判断输入的内容是否符合标准
        if (ex < 0)continue;//如果不符合标准就跳过本次循环
        
        char* ip_str = strtok(scanf_mes,",");//对输入的内容进行解析,此处为ip
        char* send_mess = strtok(NULL,",");//文件名
        strcpy(file_name,send_mess);

        struct sockaddr_in in_addr;//创建存储目标ip和端口的结构体
        bzero(&in_addr,sizeof(in_addr));//对结构体清零
        in_addr.sin_family = AF_INET;//family存放协议
        in_addr.sin_port = htons(69);//存放端口
        inet_pton(AF_INET,ip_str,&in_addr.sin_addr.s_addr);//存放ip

        unsigned char cmd[mes_len] = "";//用来存储后面拼接成的读写请求
        int len = sprintf(cmd, "%c%c%s%c%s%c", 0, 1, send_mess, 0, "octet", 0);//用sprintf创建读写请求
        sendto(*fd,cmd,len,0,(struct sockaddr *)&in_addr,sizeof(in_addr));//将需要发送的内容通过sockfd发到in_addr指定的ip

        pthread_t thread2;//创建第二个线程号
        pthread_create(&thread2,NULL,recv_tftp,fd);//运行第二个线程
        pthread_detach(thread2);//标明第二个线程结束自己默认回收资源

    }
    return 0;
}


int main(int argc, char const *argv[])
{
    int ex_argv_num = argv_ex(argc,argv[1]);//检查输入参数是否为端口
    if (ex_argv_num < 0)return 0;//否则直接返回

    int sockfd = socket(AF_INET,SOCK_DGRAM,0);//创建套接字描述符
    if (sockfd < 0)//判断是否创建成功,如果未创建成功就直接结束并打印错误信息
    {
        perror("sockfd");
        return 0;
    }
    struct sockaddr_in bind_addr;//创建需要绑定的本地ip结构体
    bzero(&bind_addr,sizeof(bind_addr));//初始化本地ip结构体
    bind_addr.sin_family = AF_INET;//协议
    bind_addr.sin_port = htons(atoi(argv[1]));//通过输入的参数绑定本地端口
    bind_addr.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY 绑定本地机的所有IP地址 
    int bind_T = bind(sockfd, (struct sockaddr *)&bind_addr, sizeof(bind_addr));// 为套接字描述符绑定本地ip和端口
    if (bind_T < 0)//通过bind的返回值判断是否绑定成功,未成功就关闭描述符并且打印错误信息以及直接结束
    {
        perror("bind");
        close(sockfd);
        return 0;
    }

    pthread_t thread1;//创建第一个线程号

    pthread_create(&thread1,NULL,send_tftp,&sockfd);//绑定线程号和函数,并且调用线程,传入参数描述符
    
    pthread_join(thread1,NULL);//主进程等待发送线程的结束

    close(sockfd);//最后关闭描述符


    return 0;
}
